package jp.co.nes.awssupport.business.logic.description;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

import jp.co.nes.awssupport.business.bean.BillingCsvBean;
import jp.co.nes.awssupport.business.constant.AwsProductName;
import jp.co.nes.awssupport.business.constant.LineItemType;
import jp.co.nes.awssupport.business.utility.AwsProductNameUtil;
import jp.co.nes.awssupport.business.utility.DescriptionInfoHelper;
import jp.co.nes.awssupport.business.utility.RegionUtil;
import jp.co.nes.awssupport.business.utility.VolumingPlanHelper;
import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.bean.ReservedManageBean;
import jp.co.nes.awssupport.common.utility.AppInfoUtil;

/**
 * 説明作成の基底クラスです。
 */
public class BaseDescriptionImpl implements Description {

    /** Full-width colon(全角コロン) */
    protected static final String FWC = "：";

    /** 説明情報ヘルパー */
    protected DescriptionInfoHelper descriptionInfoHelper;

    /** ボリューミングヘルパー */
    protected VolumingPlanHelper volumingPlanHelper;

    /** ワーニングメッセージリスト */
    protected List<String> warnMessageList;

    /**
     * デフォルトコンストラクタです。
     */
    public BaseDescriptionImpl() {
        init();
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.business.logic.description.Description#getForReservedUpfront(jp.co.nes.awssupport.business.bean.BillingCsvBean, jp.co.nes.awssupport.common.bean.ReservedManageBean)
     */
    @Override
    public String getForReservedUpfront(BillingCsvBean csvBean, ReservedManageBean rmBean) {
        return createDescriptionForReserved(csvBean, rmBean);
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.business.logic.description.Description#getForReservedMonthlyFixed(jp.co.nes.awssupport.business.bean.BillingCsvBean, jp.co.nes.awssupport.common.bean.ReservedManageBean)
     */
    @Override
    public String getForReservedMonthlyFixed(BillingCsvBean csvBean, ReservedManageBean rmBean) {
        return createDescriptionForReserved(csvBean, rmBean);
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.business.logic.description.Description#getForReservedMetered(jp.co.nes.awssupport.business.bean.BillingCsvBean, jp.co.nes.awssupport.common.bean.ReservedManageBean)
     */
    @Override
    public String getForReservedMetered(BillingCsvBean csvBean, ReservedManageBean rmBean) {
        return createDescriptionForReserved(csvBean, rmBean);
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.business.logic.description.Description#getForBlendedReserved(jp.co.nes.awssupport.business.bean.BillingCsvBean)
     */
    @Override
    public String getForBlendedReserved(BillingCsvBean csvBean) {
        return getForOnDemand(csvBean);
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.business.logic.description.Description#getForSpot(jp.co.nes.awssupport.business.bean.BillingCsvBean)
     */
    @Override
    public String getForSpot(BillingCsvBean csvBean) {
        String result = null;
        String productName = csvBean.getProductName();
        String usageType = csvBean.getUsageType();
        if (volumingPlanHelper.isApply(productName, usageType)) {
            result = createDescriptionForVolumingPlan(csvBean);
        } else {
            result = csvBean.getUsageType();
        }
        return result;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.business.logic.description.Description#getForOnDemand(jp.co.nes.awssupport.business.bean.BillingCsvBean)
     */
    @Override
    public String getForOnDemand(BillingCsvBean csvBean) {
        String result = null;
        String productName = csvBean.getProductName();
        String usageType = csvBean.getUsageType();
        if (volumingPlanHelper.isApply(productName, usageType)) {
            result = createDescriptionForVolumingPlan(csvBean);
        } else {
            result = createDescriptionForOnDemand(csvBean);
        }
        return result;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.business.logic.description.Description#getWarnMessageList()
     */
    @Override
    public List<String> getWarnMessageList() {
        return warnMessageList;
    }

    /**
     * 初期処理です。
     */
    protected void init() {
        if (descriptionInfoHelper == null) {
            descriptionInfoHelper = new DescriptionInfoHelper();
            descriptionInfoHelper.load();
        }
        if (volumingPlanHelper == null) {
            volumingPlanHelper = new VolumingPlanHelper();
        }
        if (warnMessageList == null) {
            warnMessageList = new ArrayList<>();
        }
    }

    /**
     * リザーブド関連の説明作成処理です。<br>
     * 本メソッドでは、請求CSVの使用タイプを返却していますので、製品名(リザーブドが存在する製品)に対応した説明を、
     * サブクラスで実装する必要があります。<br>
     * 現状、存在しているサブクラスの実装は、以下の内容です。<br>
     *
     * ■EC2の場合({@link AmazonElasticComputeCloudDescriptionImpl})<br>
     * 《リージョンコード-》「リザーブドインスタンス」：(※1)：インスタンスタイプ：要求クラス：期間：要求タイプ(※2)：(※3)<br>
     * ※1 オペレーションから変換した文字列(OS結合 + プリインストールSW + ライセンスモデル)<br>
     * ※2 購入オプション(「全前払い」or「一部前払い」or「前払いなし」)<br>
     * ※3 「前払い」or「毎月」or「リザーブド適用」(要求クラスがexconvertibleの場合は「交換」)<br>
     *
     * ■ElastiCacheの場合({@link AmazonElastiCacheDescriptionImpl})<br>
     * 《リージョンコード-》「リザーブドキャッシュノード」：(※1)：インスタンスタイプ：期間：要求タイプ(※2)：(※3)<br>
     * ※1 オペレーションから変換した文字列(キャッシュエンジン)<br>
     * ※2 購入オプション(「軽度使用」or「中度使用」or「重度使用」)<br>
     * ※3 「前払い」or「毎月」or「リザーブド適用」<br>
     *
     * ■Redshiftの場合({@link AmazonRedshiftDescriptionImpl})<br>
     * 《リージョンコード-》「リザーブドインスタンス」：インスタンスタイプ：期間：要求タイプ(※1)：(※2)<br>
     * ※1 購入オプション(「全前払い」or「一部前払い」or「前払いなし」)<br>
     * ※2 「前払い」or「毎月」or「リザーブド適用」<br>
     *
     * ■RDSの場合({@link AmazonRelationalDatabaseServiceDescriptionImpl})<br>
     * 《リージョンコード-》「リザーブドDBインスタンス」：(※1)：(※2)：インスタンスタイプ：期間：要求タイプ(※3)：(※4)<br>
     * ※1 オペレーションから変換した文字列(DB種別 + DBエディション + ライセンスモデル)<br>
     * ※2 「シングルAZ配置」or「マルチAZ配置」<br>
     * ※3 購入オプション(「全前払い」or「一部前払い」or「前払いなし」)<br>
     * ※4 「前払い」or「毎月」or「リザーブド適用」<br>
     *
     * ■DynamoDBの場合({@link AmazonDynamoDBDescriptionImpl})<br>
     * 《リージョンコード-》「リザーブドキャパシティー」：(※1)：期間：(※2)<br>
     * ※1 インスタンスタイプがdynamodb.readの場合は「読み込み」、dynamodb.wirteの場合は「書き込み」
     * ※2 「前払い」or「毎月」or「リザーブド適用」<br>
     *
     * ■ElasticsearchServiceの場合({@link AmazonElasticsearchServiceDescriptionImpl})<br>
     * 《リージョン-》「リザーブドインスタンス」：インスタンスタイプ(※1)：期間：要求タイプ(※2)：(※3)<br>
     * ※1 使用タイプに「ESInstance」が存在 and 末尾に「.elasticsearch」が存在しない場合は、「.elasticsearch」を追加<br>
     * ※2 購入オプション(「全前払い」or「一部前払い」or「前払いなし」)<br>
     * ※3 「前払い」or「毎月」or「リザーブド適用」<br>
     *
     * @param csvBean 請求CSV
     * @param rmBean リザーブド管理
     * @return 説明
     */
    protected String createDescriptionForReserved(BillingCsvBean csvBean, ReservedManageBean rmBean) {
        return getDefaultDescriptionValue(csvBean);
    }

    /**
     * ボリューミングプランに関する説明を作成する処理です。
     *
     * @param csvBean 請求CSV
     * @return 説明
     */
    protected String createDescriptionForVolumingPlan(BillingCsvBean csvBean) {
        String result = getDefaultDescriptionValue(csvBean);

        String productName = csvBean.getProductName();
        String usageType = csvBean.getUsageType();

        String regionCode = null;
        if (volumingPlanHelper.isRegionCodeAddition(productName, usageType)) {
            UsageTypeTokenizer usageTypeTokenizer = new UsageTypeTokenizer(productName, usageType);
            regionCode = usageTypeTokenizer.getRegionCode();
        }

        String usageTypeSuffix = null;
        if (AwsProductNameUtil.isEquals(AwsProductName.CLOUD_FRONT, productName)
                && usageType.endsWith(VolumingPlanHelper.MERGE_USAGE_TYPE_SUFFIX)) {
            usageTypeSuffix = usageType;
        } else {
            usageTypeSuffix = volumingPlanHelper.getUsageTypeSuffix(productName, usageType);
        }

        String liDescription = csvBean.getLineItemDescription();
        String usageTypeValue = descriptionInfoHelper.getForVolumingPlan(productName, liDescription, usageTypeSuffix);
        if (usageTypeValue == null) {
            // 使用タイプ(ボリューミングプラン)に対応する説明が存在しなかった場合
            addWarnMessage(csvBean);
            return result;
        }

        StringBuilder builder = new StringBuilder();
        builder.append(getDescriptionPrefix(regionCode));
        builder.append(usageTypeValue);
        result = builder.toString();

        return result;
    }

    /**
     * オンデマンドに関する説明を作成する処理です。
     *
     * @param csvBean 請求CSV
     * @return 説明
     */
    protected String createDescriptionForOnDemand(BillingCsvBean csvBean) {
        String result = getDefaultDescriptionValue(csvBean);

        String productName = csvBean.getProductName();
        String usageType = csvBean.getUsageType();
        UsageTypeTokenizer usageTypeTokenizer = new UsageTypeTokenizer(productName, usageType);

        StringBuilder builder = new StringBuilder();
        String usageTypeValue = descriptionInfoHelper.get(productName, usageType);
        if (usageTypeValue == null) {
            // 使用タイプに対応する説明が存在しなかった場合
            String regionCode = usageTypeTokenizer.getRegionCode();
            if (StringUtils.isEmpty(regionCode)) {
                // リージョンコードが存在しなかった場合、使用タイプを分割した値に対応する内容で説明を作成する。
                String value = buildDescriptionFromUsageType(regionCode, productName, usageTypeTokenizer);
                if (!StringUtils.isEmpty(value)) {
                    builder.append(value);
                }
            } else {
                // リージョンコードが存在した場合
                String regionCodeExclusionPart = usageTypeTokenizer.getRegionCodeExclusionPart();
                String regionCodeExclusionPartValue = descriptionInfoHelper.get(productName,
                        regionCodeExclusionPart);
                if (regionCodeExclusionPartValue == null) {
                    // 使用タイプからリージョンコードを除く内容に対応する説明が存在しなかった場合、
                    // 使用タイプを分割した値に対応する内容で説明を作成する。
                    String value = buildDescriptionFromUsageType(regionCode, productName, usageTypeTokenizer);
                    if (!StringUtils.isEmpty(value)) {
                        builder.append(value);
                    }
                } else {
                    // 使用タイプからリージョンコードを除く内容に対応する説明が存在した場合、
                    // 使用タイプからリージョンコードを除く内容で説明を作成する
                    builder.append(regionCode).append("-");
                    builder.append(regionCodeExclusionPartValue);
                }
            }
        } else {
            // 使用タイプに対応する説明が存在した場合
            builder.append(usageTypeValue);
        }

        String description = builder.toString();
        if (StringUtils.isEmpty(description)) {
            addWarnMessage(csvBean);
        } else {
            result = description;
        }

        return result;
    }

    /**
     * 使用タイプから説明を作成する処理です。
     *
     * @param regionCode リージョンコード
     * @param productName 製品名
     * @param usageTypeTokenizer 使用タイプ分割
     * @return 説明
     */
    protected String buildDescriptionFromUsageType(String regionCode, String productName,
            UsageTypeTokenizer usageTypeTokenizer) {
        String kindValue = descriptionInfoHelper.get(productName, usageTypeTokenizer.getKind());
        if (kindValue == null) {
            return null;
        }

        String instanceType = usageTypeTokenizer.getInstanceType();
        if (instanceType == null) {
            return null;
        }

        String operation = usageTypeTokenizer.getOperation();
        String operationValue = descriptionInfoHelper.get(productName, operation);
        if (operationValue == null && operation != null) {
            return null;
        }

        StringBuilder builder = new StringBuilder();
        builder.append(getDescriptionPrefix(regionCode));
        builder.append(kindValue);
        if (operationValue != null) {
            builder.append(FWC).append(operationValue);
        }
        builder.append(FWC).append(instanceType);
        return builder.toString();
    }

    /**
     * 種類文字列に対応する日本語文字列を返却します(リザーブド用)。
     *
     * @param productName 製品名
     * @return リザーブドに関する種類文字列
     */
    protected String getReservedInstanceValue(String productName) {
        return descriptionInfoHelper.get(productName, "ReservedInstance");
    }

    /**
     * オペレーションに対応する日本語文字列を返却します(リザーブド用)。
     *
     * @param productName 製品名
     * @param rmBean リザーブド管理
     * @return オペレーションに関する文字列
     */
    protected String getOperationValueForReserved(String productName, ReservedManageBean rmBean) {
        String productDescription = rmBean.getProductDescription();
        if (StringUtils.isEmpty(productDescription)) {
            return null;
        }

        Map<String, String> map = AppInfoUtil.getMap("ProductDescriptionTypeValueMapping");
        String operation = map.get(productDescription);
        if (StringUtils.isEmpty(operation)) {
            return null;
        }

        String operationValue = descriptionInfoHelper.get(productName, operation);
        if (operationValue == null) {
            return null;
        }
        return operationValue;
    }

    /**
     * 請求の種類に対応する日本語文字列を返却します(リザーブド用)。
     *
     * @param productName
     * @param lineItemType 請求の種類
     * @return 請求の種類に関する文字列
     */
    protected String getLineItemTypeValueForReserved(String productName, LineItemType lineItemType) {
        String result = null;
        if (lineItemType == LineItemType.FEE || lineItemType == LineItemType.RI_FEE
                || lineItemType == LineItemType.DISCOUNTED_USAGE) {
            result = descriptionInfoHelper.get(productName, lineItemType.name());
        }
        return result;
    }

    /**
     * 説明のプリフィックスを返却します。
     *
     * @param regionCode リージョンコード
     * @return 説明のプリフィックス
     */
    protected String getDescriptionPrefix(String regionCode) {
        String result = "";
        if (!StringUtils.isEmpty(regionCode)) {
            result = regionCode + "-";
        }
        return result;
    }

    /**
     * 説明(デフォルト)を返却します。
     *
     * @param csvBean 請求CSV
     * @return 説明(デフォルト)
     */
    protected String getDefaultDescriptionValue(BillingCsvBean csvBean) {
        // TODO デフォルトは使用タイプでよいか？
        return csvBean.getUsageType();
    }

    /**
     * 説明作成中に発生したワーニングに関するメッセージを追加します。
     *
     * @param csvBean 請求CSV
     */
    protected void addWarnMessage(BillingCsvBean csvBean) {
        StringBuilder warnMessage = new StringBuilder();
        warnMessage.append("failed convert of description(linkedAccountId=").append(csvBean.getLinkedAccountId());
        warnMessage.append(",productName=").append(csvBean.getProductName());
        warnMessage.append(",usageType=").append(csvBean.getUsageType());
        warnMessage.append(",lineItemDescription=").append(csvBean.getLineItemDescription()).append(")");
        if (!warnMessageList.stream().anyMatch(s -> s.equals(warnMessage.toString()))) {
            warnMessageList.add(warnMessage.toString());
        }
    }

    /**
     * 使用タイプ分割クラスです。<br>
     * 使用タイプから説明を作成する際に、使用タイプを分割し、その情報を保持します。
     */
    protected class UsageTypeTokenizer {

        /** リージョンコード */
        private String regionCode;

        /** 種類文字列 */
        private String kind;

        /** インスタンスタイプ */
        private String instanceType;

        /** オペレーション */
        private String operation;

        /** 使用タイプからリージョンコードを除いた部分 */
        private String regionCodeExclusionPart;

        /**
         * 製品名、使用タイプを引数に持つコンストラクタです。
         *
         * @param productName 製品名
         * @param usageType 使用タイプ
         */
        public UsageTypeTokenizer(String productName, String usageType) {
            List<String> regionCodeList = RegionUtil.getRegionCodeList();
            if (regionCodeList.stream().anyMatch(rc -> usageType.startsWith(rc))) {
                // 使用タイプの先頭にリージョンが存在する場合、keyに使用タイプからリージョンを除外した値を設定
                int index = usageType.indexOf("-");
                if (index < 0) {
                    // 通常発生しない
                    throw new CommonException("usageType is wrong(usageType=" + usageType + ")");
                }
                regionCode = usageType.substring(0, index);
                regionCodeExclusionPart = usageType.substring(index + 1);
            } else {
                regionCodeExclusionPart = usageType;
            }

            String[] splitValues = regionCodeExclusionPart.split(":");
            for (int i = 0; i < splitValues.length; i++) {
                String splitValue = splitValues[i];
                if (i == 0) {
                    kind = splitValue;
                } else if (i == 1) {
                    instanceType = splitValue;
                } else if (i == 2) {
                    operation = splitValue;
                } else if (i == 3) {
                    operation = operation + ":" + splitValue;
                }
            }
        }

        /**
         * リージョンコードを返却します。
         *
         * @return リージョンコード
         */
        public String getRegionCode() {
            return regionCode;
        }

        /**
         * 種類文字列を返却します。
         *
         * @return 種類文字列
         */
        public String getKind() {
            return kind;
        }

        /**
         * インスタンスタイプを返却します。
         *
         * @return インスタンスタイプ
         */
        public String getInstanceType() {
            return instanceType;
        }

        /**
         * オペレーションを返却します。
         *
         * @return オペレーション
         */
        public String getOperation() {
            return operation;
        }

        /**
         * 使用タイプからリージョンコードを除いた部分を返却します。
         *
         * @return 使用タイプからリージョンコードを除いた部分
         */
        public String getRegionCodeExclusionPart() {
            return regionCodeExclusionPart;
        }
    }
}
